// Copyright (c) Omar AL Zabir. All rights reserved.
// For continued development and updates, visit http://msmvps.com/omar

using System;
using System.Reflection;
using System.Web;
using System.Web.Caching;
using System.Collections;
using System.Web.Services;
using System.Web.Services.Protocols;
using System.Web.Script.Services;

using System.Linq;
using System.Xml.Linq;
using System.Xml;
using System.Net;
using System.IO;
using System.IO.Compression;
using System.Net.Sockets;

using AJAXASMXHandler;
using System.Text;
using System.Text.RegularExpressions;
using Rss;

/// <summary>
/// Summary description for Proxy
/// </summary>
[WebService(Namespace = "http://nexus.nus.edu.sg/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[ScriptService]
public class ProxyAsync : System.Web.Services.WebService
{
    public const string CACHE_KEY = "ProxyAsync.";

    public ProxyAsync()
    {
    }

    private class GetStringState : AsyncWebMethodState
    {
        public HttpWebRequest Request;
        public string Url;
        public int CacheDuration;
        public GetStringState(object state) : base(state) {}
    }

    [ScriptMethod]
    public IAsyncResult BeginGetString(string url, int cacheDuration, AsyncCallback cb, object state)
    {
        // See if the response from the URL is already cached on server
        string cachedContent = Context.Cache[CACHE_KEY+url] as string;
        if (!string.IsNullOrEmpty(cachedContent))
        {
            this.CacheResponse(Context, cacheDuration);
            return new AsmxHandlerSyncResult(cachedContent);
        }

        HttpWebRequest request = this.CreateHttpWebRequest(url);
        // As we will stream the response, don't want to automatically decompress the content
        request.AutomaticDecompression = DecompressionMethods.None;

        GetStringState myState = new GetStringState(state);
        myState.Request = request;
        myState.Url = url;
        myState.CacheDuration = cacheDuration;

        return request.BeginGetResponse(cb, myState);
    }

    [ScriptMethod]
    public string EndGetString(IAsyncResult result)
    {
        GetStringState state = result.AsyncState as GetStringState;
        MemoryStream responseBuffer = new MemoryStream();
 
        HttpWebRequest request = state.Request;
        using( HttpWebResponse response = request.EndGetResponse(result) as HttpWebResponse )
        {
            using( Stream stream = response.GetResponseStream() )
            {
                // produce cache headers for response caching
                this.CacheResponse(state.Context, state.CacheDuration);
                
                string contentLength = response.GetResponseHeader("Content-Length") ?? "-1";
                state.Context.Response.AppendHeader("Content-Length", contentLength);

                string contentEncoding = response.GetResponseHeader("Content-Encoding") ?? "";
                state.Context.Response.AppendHeader("Content-Encoding", contentEncoding);

                state.Context.Response.ContentType = response.ContentType;
                               
                const int BUFFER_SIZE = 4 * 1024;
                byte[] buffer = new byte[BUFFER_SIZE];
                int dataReceived;
                while ((dataReceived = stream.Read(buffer, 0, BUFFER_SIZE)) > 0)
                {
                    if (!state.Context.Response.IsClientConnected) return string.Empty;

                    // Transmit to client (browser) immediately
                    byte[] outBuffer = new byte[dataReceived];
                    Array.Copy(buffer, outBuffer, dataReceived);

                    state.Context.Response.BinaryWrite(outBuffer);
                    //state.Context.Response.Flush();

                    // Store in buffer so that we can cache the whole stuff
                    responseBuffer.Write(buffer, 0, dataReceived);
                }
                
                // If the content is compressed, decompress it
                Stream contentStream = contentEncoding == "gzip" ? 
                    (new GZipStream(responseBuffer, CompressionMode.Decompress) as Stream)
                    : 
                    (contentEncoding == "deflate" ? 
                        (new DeflateStream(responseBuffer, CompressionMode.Decompress) as Stream) 
                        : 
                        (responseBuffer as Stream));

                // Cache the decompressed content so that we can return it next time
                using (StreamReader reader = new StreamReader(contentStream, true))
                {
                    string content = reader.ReadToEnd();

                    state.Context.Cache.Insert(state.Url, content, null,
                    Cache.NoAbsoluteExpiration,
                    TimeSpan.FromMinutes(state.CacheDuration),
                    CacheItemPriority.Normal, null);
                }

                state.Context.Response.Flush();

                return null;
            }
        }        
    }

    [WebMethod]
    [ScriptMethod(UseHttpGet=true)]
    public string GetString(string url, int cacheDuration)
    {
        using (WebClient client = new WebClient())
        {
            return client.DownloadString(url);
        }
    }

    [WebMethod]
    [ScriptMethod(UseHttpGet = true, ResponseFormat=ResponseFormat.Xml)]
    public string GetXml(string url, int cacheDuration)
    {
        return GetString(url, cacheDuration);
    }

    [ScriptMethod]
    public IAsyncResult BeginGetXml(string url, int cacheDuration, AsyncCallback cb, object state)
    {
        return BeginGetString(url, cacheDuration, cb, state);
    }

    [ScriptMethod]
    public string EndGetXml(IAsyncResult result)
    {
        return EndGetString(result);
    }

    [WebMethod]
    [ScriptMethod(UseHttpGet = true)]
    public object GetRss(string url, int count, int cacheDuration)
    {
       return GetRssFeed(url, count, cacheDuration, true);
    }
    [WebMethod]
    [ScriptMethod(UseHttpGet = true)]
    public object GetRssFeed(string url, int count, int cacheDuration, bool StripHTML)
    {
       using (new EntLib.EntTimedLog("", "ProxyAsync.GetRSS", url))
       {
          if (count < 1)
             count = int.MaxValue;

          var obj = Context.Cache[CACHE_KEY + url];
          ArrayList Items;
          obj = null;
          if (obj == null)
             Items = new ArrayList();
          else
             Items = obj as ArrayList;

          try
          {
             if (obj == null)
             {
                if (url.Length > 0)
                {
                   RssFeed feed = RssFeed.Read(url);

                   int ChannelLen = feed.Channels.Count;
                   for (int i = 0; i < ChannelLen; i++)
                   {
                      var ChannelElement = (RssChannel)feed.Channels[i];
                      int ItemLen = ChannelElement.Items.Count;

                      for (int j = 0; j < ItemLen; j++)
                      {
                         var Element = ChannelElement.Items[j];
                         var Item = new RssItem();
                         if (StripHTML)
                            Item.Description = StripTags(Element.Description);
                         else
                            Item.Description = Element.Description;

                         Item.Link = Element.Link.ToString();
                         if (StripHTML)
                            Item.Title = StripTags(Element.Title);
                         else
                            Item.Title = Element.Title;

                         Item.ContentEncoded = Element.ContentEncoded;
                         Items.Add(Item);
                      }
                   }
                   Context.Cache.Insert(CACHE_KEY + url, Items, null, DateTime.MaxValue, TimeSpan.FromMinutes(15));
                }
             }

             int Len = Math.Min(Items.Count, count);
             RssItem[] result = new RssItem[Len];
             Items.CopyTo(0, result, 0, Len);
             return result;
          }
          catch (Exception ex)
          {
             Items.Clear();
             RssItem[] result = new RssItem[1];
             if (ex.Message == "The remote server returned an error: (404) Not Found.")
             {
                result[0] = new RssItem();
                result[0].Description = ex.Message;
                result[0].Title = "The URL: " + url + " cannot be found.";
                result[0].Link = "";
                Context.Cache[CACHE_KEY + url] = Items;
             }
             else
             {
                result[0] = new RssItem();
                result[0].Description = ex.Message;
                result[0].Title = "Error occured during RSS Get, click to send email to support";
                result[0].Link = "/support/?error=" + Server.UrlEncode("RSS Error: " + url + Environment.NewLine + ex.ToString());
                Context.Cache[CACHE_KEY + url] = Items;
             }
             return result;
          }
          finally
          {
             this.CacheResponse(Context, cacheDuration);
          }
       }
    }

    private void CacheResponse(HttpContext context, int durationInMinutes)
    {
        TimeSpan duration = TimeSpan.FromMinutes(durationInMinutes);
        
        // With the new AJAX ASMX handler, there's no need for this hack to set maxAge value
        /*FieldInfo maxAge = HttpContext.Current.Response.Cache.GetType().GetField("_maxAge", BindingFlags.Instance | BindingFlags.NonPublic);
        maxAge.SetValue(HttpContext.Current.Response.Cache, duration);*/

        context.Response.Cache.SetCacheability(HttpCacheability.Public);
        context.Response.Cache.SetExpires(DateTime.Now.Add(duration));
        context.Response.Cache.AppendCacheExtension("must-revalidate, proxy-revalidate");
        context.Response.Cache.SetMaxAge(duration);
    }

    private HttpWebRequest CreateHttpWebRequest(string url)
    {
        HttpWebRequest request = WebRequest.Create(url) as HttpWebRequest;
        request.Headers.Add("Accept-Encoding", "gzip");
        request.AutomaticDecompression = DecompressionMethods.GZip;
        request.MaximumAutomaticRedirections = 2;
        request.MaximumResponseHeadersLength = 4 * 1024;
        request.ReadWriteTimeout = 1 * 1000;
        request.Timeout = 5 * 1000;

        return request;
    }

    public static string StripTags(string html, bool LimitTo500)
    {
       string result = Functions.StripHtml(html);
       if (LimitTo500 && result.Length > 500)
          return result.Substring(0, 497) + "...";
       else
          return result;
    }

    public static string StripTags(string html)
    {
       return StripTags(html, true);
    }
}
